﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using ThePresenter.Net.PresenterCom.Uids;

namespace ThePresenter.Net.NetCom
{
	public class Client
	{
		//HACK: DebugClass
		private DebugClass _D;

		#region Fields
		// Info about client
		private ClientInfo _Info;
		private Uid _LocalId = new Uid();
		private Mutex _DisconnectingMutex;

		private Boolean _WasClosed = false;
		private Mutex _ClosingSignal;
		
		// Network fields
		private NetworkStream _Stream;
		private Socket _StreamSocket;

		// Sending fields
		private Boolean _EndSender = false;
		private Thread _SenderThread;
		private ManualResetEvent _SenderSleep;
		private Queue<byte[]> _DataQueue;
		#endregion


		#region Properties

		/// <summary>
		/// Gets informations about client.
		/// </summary>
		/// <value>The info.</value>
		public ClientInfo Info
		{
			get { return _Info; }
		}

		/// <summary>
		/// Gets a value indicating whether informations about client are complete.
		/// </summary>
		/// <value>
		/// 	<c>true</c> if informations are complete; otherwise, <c>false</c>.
		/// </value>
		public Boolean IsComplete
		{
			get { return _Info.IsComplete; }
		}

		/// <summary>
		/// Gets a value indicating whether this <see cref="Client"/> is connected.
		/// </summary>
		/// <value><c>true</c> if connected; otherwise, <c>false</c>.</value>
		public Boolean IsConnected
		{
			get { return this._StreamSocket.Connected; }
		}

		/// <summary>
		/// Gets a value indicating whether this instance is disconnecting.
		/// </summary>
		/// <value>
		/// 	<c>true</c> if this instance is disconnecting; otherwise, <c>false</c>.
		/// </value>
		public Boolean IsDisconnecting
		{
			get;
			private set;
		}

		/// <summary>
		/// Gets a value indicating whether this instance is receiving.
		/// </summary>
		/// <value>
		/// 	<c>true</c> if this instance is receiving; otherwise, <c>false</c>.
		/// </value>
		public Boolean IsReceiving
		{
			get;
			private set;
		}

		/// <summary>
		/// Gets a value indicating whether this instance is sending data from internal data queue.
		/// </summary>
		/// <value>
		/// 	<c>true</c> if this instance is sending; otherwise, <c>false</c>.
		/// </value>
		public Boolean IsSending
		{
			get;
			private set;
		}

		/// <summary>
		/// Gets the local id of client.
		/// </summary>
		/// <value>Id generated localy.</value>
		public Uid LocalId
		{
			get { return _LocalId; }
		}

		/// <summary>
		/// Gets the remote id of client.
		/// </summary>
		/// <value>Id sent by client.</value>
		public Uid RemoteId
		{
			get { return _Info.Uid; }
		}

		#endregion


		#region Events

		/// <summary>
		/// IncomingData event handler delegate
		/// </summary>
		/// <param name="sender">Object representing source of data</param>
		/// <param name="data">Data received</param>
		public delegate void ClientIncomingDataHandler(object sender, byte[] data);

		/// <summary>
		/// Event fired when complete data are received
		/// </summary>
		public event ClientIncomingDataHandler IncomingData;

		/// <summary>
		/// OnDisconnected event handler delegate
		/// </summary>
		public delegate void ClientDisconnectedEventHandler(Uid sender);

		/// <summary>
		/// Occurs when client disconnects.
		/// </summary>
		public event ClientDisconnectedEventHandler OnDisconnected; 

		#endregion


		#region Constructor

		/// <summary>
		/// Initializes a new instance of the <see cref="Client"/> class.
		/// </summary>
		/// <param name="S">Socket connected to remote client.</param>
		public Client(Socket S)
		{
			//HACK: DebugClass
			this._D = new DebugClass("Client{" + this._LocalId.ToString() + "}");

			// Fill info about client
			this._Info = new ClientInfo((IPEndPoint)S.RemoteEndPoint);
			this._ClosingSignal = new Mutex();
			
			// Listening initialization
			this._StreamSocket = S;
			this._Stream = new NetworkStream(this._StreamSocket, true);
			this._DisconnectingMutex = new Mutex(false);

			// Sending initialization
			this._DataQueue = new Queue<byte[]>();
			this._SenderSleep = new ManualResetEvent(true);
			this._SenderThread = new Thread(new ThreadStart(QueueSender));
			this._SenderThread.Name = "SenderThread";
		}

		#endregion


		#region Methods

		#region public

		/// <summary>
		/// Disconnects client correctly from remote host.
		/// </summary>
		public void Disconnect()
		{
			this.Disconnect(false);
		}

		/// <summary>
		/// Disconnects client.
		/// </summary>
		/// <param name="Remote">if set to <c>true</c> [remote].</param>
		private void Disconnect(bool Remote)
		{
			try
			{
				this._DisconnectingMutex.WaitOne();
			}
			catch (AbandonedMutexException)
			{
			}

			if (!this.IsDisconnecting && !this._WasClosed)
			{
				this.IsDisconnecting = true;
				_D.Write("{0} disconnecting...", this._LocalId.ToString());

				this.EndSender();

				if (!Remote)
				{
					this._StreamSocket.Close();
				}

				this._WasClosed = true;
				_D.Write("{0} disconnected.", this._LocalId.ToString());

				if (this.OnDisconnected != null) this.OnDisconnected(this.LocalId);

				this.IsDisconnecting = false;
				this._DisconnectingMutex.ReleaseMutex();
			}
		}

		/// <summary>
		/// Fills in logical info about remote client.
		/// </summary>
		/// <param name="_Uid">The remote uid of client.</param>
		/// <param name="_Name">Name of remote client.</param>
		public void FillLogicalInfo(Uid _Uid, string _Name)
		{
			this._Info.FillLogicalInfo(_Uid, _Name);
		}

		/// <summary>
		/// Enqueues data to internal data queue and wakes up sender thread.
		/// </summary>
		/// <param name="Data">The data.</param>
		public void Send(byte[] Data)
		{
			this._DataQueue.Enqueue(Data);
			this._SenderSleep.Set();
		}

		/// <summary>
		/// Starts receiveing data from remote client and sending data to remote client.
		/// </summary>
		public void Start()
		{
			// Start receiving
			ClientReceiveDataState State = new ClientReceiveDataState(_Stream);
			_Stream.BeginRead(State.LengthBuffer, 0, ClientReceiveDataState.DATALENGTH_SIZE, this.OnLengthReceive, State);
			this.IsReceiving = true;

			// Start sending
			this._SenderThread.Start();
		}

		/// <summary>
		/// Stops receiveing data from remote client and sending data to remote client.
		/// </summary>
		//public void Stop()
		//{
		//    // Stop listening
		//    this._Stream.Close();
		//    this.IsReceiving = false;

		//    // Stop sending
		//    this.EndSender();
		//}

		#endregion

		#region private

		/// <summary>
		/// Ends the sender.
		/// </summary>
		private void EndSender()
		{
			// End sender thread
			this._EndSender = true;
			this._DataQueue.Clear();
			this._SenderSleep.Set();
		}

		/// <summary>
		/// Directly sends data to remote client avoiding dataqueue.
		/// This method should not be used directly, unless necessary. Use Send() instead.
		/// </summary>
		/// <param name="DataLength">Length of the data.</param>
		/// <param name="Data">The data.</param>
		private void DirectSend(byte[] DataLength, byte[] Data)
		{
			if (Data.Length > 0)
			{
				lock (_Stream)
				{
					try
					{
						if (_Stream.CanWrite)
						{
							_Stream.Write(DataLength, 0, DataLength.Length);
							_Stream.Write(Data, 0, Data.Length);
						}
						else
						{
							this.Disconnect(false);
						}
					}
					catch (ObjectDisposedException E)
					{
						this._DataQueue.Clear();
						this._EndSender = true;
					}
				}
			}
		}

		/// <summary>
		/// Sends data from internal data queue.
		/// </summary>
		private void QueueSender()
		{
			byte[] Data;
			byte[] DataLength;

			while (true)
			{
				this.IsSending = true;
				this._SenderSleep.WaitOne();

				if (this._EndSender)
				{
					this.IsSending = false;
					break;
				}

				if (!this.IsSending) // If Server is not broadcasting but someone awoke thread just go to sleep again
				{
					this._SenderSleep.Reset();
					continue;
				}

				while (this._DataQueue.Count > 0)
				{
					Data = this._DataQueue.Dequeue();
					DataLength = BitConverter.GetBytes(Data.Length);
					this.DirectSend(DataLength, Data);
				}

				if (this._EndSender)
				{
					this.IsSending = false;
					break;
				}

				this._SenderSleep.Reset();
			}
		}

		#endregion

		#endregion

		#region Callbacks

		/// <summary>
		/// Called when client receives information about length of incoming data.
		/// </summary>
		private void OnLengthReceive(IAsyncResult Result)
		{
			ClientReceiveDataState State = (ClientReceiveDataState)Result.AsyncState;

			try
			{
				State.CurrentDataBlockSize = State.Stream.EndRead(Result);
			}
			catch (IOException)
			{
				this.Disconnect(true);
				return;
			}

			// If size is 0
			if (State.CurrentDataBlockSize == 0)
			{
				this.Disconnect(true);
				return;
			}

			State.DataSizeTotal = BitConverter.ToInt32(State.LengthBuffer, 0);
			State.Data = new MemoryStream(State.DataSizeTotal);

			State.DataBuffer = new byte[State.DataSizeRemaining];

			_D.Write("New data incoming. Total length {0}.", State.DataSizeTotal);

			State.Stream.BeginRead(State.DataBuffer, 0, State.DataSizeRemaining, this.OnDataReceive, State);
		}

		/// <summary>
		/// Called when client receives complete data.
		/// </summary>
		/// <param name="Result">The result.</param>
		private void OnDataReceive(IAsyncResult Result)
		{
			ClientReceiveDataState State = (ClientReceiveDataState)Result.AsyncState;

			try
			{
				State.CurrentDataBlockSize = State.Stream.EndRead(Result);
			}
			catch (IOException)
			{
				this.Disconnect(true);
				return;
			}

			// If size is 0
			if (State.CurrentDataBlockSize == 0)
			{
				this.Disconnect(true);
				return;
			}

			State.Data.Write(State.DataBuffer, 0, State.CurrentDataBlockSize);

			_D.Write("Received {0} bytes. {1} bytes remaining.", State.CurrentDataBlockSize, State.DataSizeRemaining);

			if (State.DataSizeRemaining > 0)
			{
				State.Stream.BeginRead(State.DataBuffer, 0, State.DataSizeRemaining, this.OnDataReceive, State);
			}
			else if (State.DataSizeRemaining == 0)
			{
				if (State.Data.Length == 0) // should not happen but one never knows
				{
					this.Disconnect(true);
					return;
				}

				if (IncomingData != null)
				{
					IncomingData(this, State.Data.ToArray());
				}

				State.Reset();
				State.Stream.BeginRead(State.LengthBuffer, 0, ClientReceiveDataState.DATALENGTH_SIZE, this.OnLengthReceive, State);
			}
			else
			{
				throw new ApplicationException("Client error", new InvalidOperationException("Received more data then excepted"));
			}

		}

		#endregion

	}
}
